// $Id$

package net.sf.persist;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Represents the mapping of columns to getters and setters of a POJO.
 * <p>
 * It is used when a class specifies a
 * {@link net.sf.persist.annotations.NoTable NoTable} annotation, which means
 * the class is not mapped to a table in the database, and will be only used to
 * store data from queries.
 */
public class NoTableMapping extends Mapping {

	// POJO class
	private final Class objectClass;

	// map field names to setters
	private final Map<String, Method> settersMap;

	// map field names to getters
	private final Map<String, Method> gettersMap;

	// map possible column names to field names
	private final Map<String, String> columnsMap;

	public NoTableMapping(Class objectClass, NameGuesser nameGuesser) {

		checkAnnotation(objectClass);

		this.objectClass = objectClass;

		// get the list of annotations, getters and setters
		Map[] fieldsMaps = Mapping.getFieldsMaps(objectClass);
		final Map<String, net.sf.persist.annotations.Column> annotationsMap = fieldsMaps[0];
		gettersMap = fieldsMaps[1];
		settersMap = fieldsMaps[2];

		// create columns map by iterating through all fields in the object
		// class
		// if a field has a @Column annotation, use it, otherwise add all
		// guessed names for the field in the map
		columnsMap = new HashMap();
		for (String fieldName : gettersMap.keySet()) {

			net.sf.persist.annotations.Column annotation = annotationsMap.get(fieldName);

			// autoGenerated is not supported on @NoTable mappings
			if (annotation != null) {
				if (annotation.autoGenerated() == true) {
					throw new PersistException("@Column(autoGenerated=true) is set for field [" + fieldName
							+ "] of class [" + objectClass.getCanonicalName()
							+ " which has been declared with @NoTable");
				}
			}

			// if there's a column name specified in the annotation, use it
			if (annotation != null && annotation.name() != null) {

				// check if the column name is blank
				if (annotation.name().trim().equals("")) {
					throw new PersistException("@Column annotation for field [" + fieldName + "] of class ["
							+ objectClass.getCanonicalName() + "] defines a blank column name");
				}

				// check for name conflicts
				checkNameConflicts(fieldName, annotation.name());

				// add to map
				columnsMap.put(annotation.name(), fieldName);
			}

			else { // no annotation, add all guessed column names for the field

				Set<String> guessedColumns = nameGuesser.guessColumn(fieldName);
				for (String guessedColumn : guessedColumns) {

					// check for name conflicts
					checkNameConflicts(fieldName, guessedColumn);

					// add to map
					columnsMap.put(guessedColumn, fieldName);
				}
			}
		}

	}

	/**
	 * Returns the field name associated with a given column. If a mapping can't
	 * be found, will throw a PersistException.
	 */
	public String getFieldNameForColumn(String columnName) {
		String fieldName = columnsMap.get(columnName);
		if (fieldName == null) {
			throw new PersistException("Could map field for column [" + columnName + "] on class ["
					+ objectClass.getCanonicalName()
					+ "]. Please specify an explict @Column annotation for that column.");
		}
		return fieldName;
	}

	/**
	 * Returns the setter method associated with a given column. If a mapping
	 * can't be found, will throw a PersistException.
	 * 
	 * @see Mapping
	 */
	public Method getSetterForColumn(String columnName) {
		String fieldName = getFieldNameForColumn(columnName);
		return settersMap.get(fieldName);
	}

	/**
	 * Returns the getter method associated with a given column. If a mapping
	 * can't be found, will throw a PersistException.
	 * 
	 * @see Mapping
	 */
	public Method getGetterForColumn(String columnName) {
		String fieldName = getFieldNameForColumn(columnName);
		return gettersMap.get(fieldName);
	}

	/**
	 * Checks if a given column name conflicts with an existing name in the
	 * columns map.
	 */
	private void checkNameConflicts(String fieldName, String column) {
		String existingFieldName = columnsMap.get(column);
		if (existingFieldName != null) {
			throw new PersistException("Fields [" + fieldName + "] and [" + existingFieldName
							+ "] have conflicting column name [" + column
							+ "] either from guessed names or anotations. "
							+ "Please specify @Column mappings for at least one of those fields");
		}
	}

	/**
	 * Checks if {@link net.sf.persist.annotations.NoTable NoTable} is present
	 * and if a conflicting {@link net.sf.persist.annotations.Table Table} is
	 * not present.
	 */
	private void checkAnnotation(Class objectClass) {
		// get @NoTable annotation
		final net.sf.persist.annotations.NoTable noTableAnnotation = (net.sf.persist.annotations.NoTable) objectClass
				.getAnnotation(net.sf.persist.annotations.NoTable.class);

		// check if annotation is set
		if (noTableAnnotation == null) {
			throw new PersistException("Class [" + objectClass.getCanonicalName()
					+ "] does not specify a @NoTable annotation therefore it can't be mapped through NoTableMapping");
		}

		// check for conflicting @Table annotation
		final net.sf.persist.annotations.Table tableAnnotation = (net.sf.persist.annotations.Table) objectClass
				.getAnnotation(net.sf.persist.annotations.Table.class);

		if (tableAnnotation != null) {
			throw new PersistException("Class [" + objectClass.getCanonicalName()
					+ "] specifies conflicting @Table and @NoTable annotations");
		}
	}

}
